/*
 * Copyright (c) 2024.  little3201.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *       https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package com.server.starter.exploiter.service.impl;

import com.server.starter.exploiter.constants.FieldConstant;
import com.server.starter.exploiter.constants.FieldTypeEnum;
import com.server.starter.exploiter.domain.ColumnInfo;
import com.server.starter.exploiter.domain.Field;
import com.server.starter.exploiter.domain.Schema;
import com.server.starter.exploiter.domain.Template;
import com.server.starter.exploiter.dto.SchemaDTO;
import com.server.starter.exploiter.mapper.ColumnMapper;
import com.server.starter.exploiter.repository.FieldRepository;
import com.server.starter.exploiter.repository.SchemaRepository;
import com.server.starter.exploiter.repository.TemplateRepository;
import com.server.starter.exploiter.service.SchemaService;
import com.server.starter.exploiter.vo.SchemaVO;
import com.server.starter.exploiter.vo.TemplateVO;
import freemarker.template.Configuration;
import freemarker.template.TemplateException;
import freemarker.template.TemplateExceptionHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import java.io.*;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

@Service
public class SchemaServiceImpl implements SchemaService {

    private static final Logger logger = LoggerFactory.getLogger(SchemaServiceImpl.class);

    private final Configuration cfg = new Configuration(Configuration.VERSION_2_3_33);

    private final SchemaRepository schemaRepository;
    private final FieldRepository fieldRepository;
    private final TemplateRepository templateRepository;

    private final ColumnMapper columnMapper;

    public SchemaServiceImpl(SchemaRepository schemaRepository, FieldRepository fieldRepository,
                             TemplateRepository templateRepository, ColumnMapper columnMapper) {
        this.schemaRepository = schemaRepository;
        this.fieldRepository = fieldRepository;
        this.templateRepository = templateRepository;
        this.columnMapper = columnMapper;

        cfg.setDefaultEncoding("UTF-8");
        cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
    }

    @Override
    public Page<SchemaVO> retrieve(int page, int size, String sortBy, boolean descending, String name) {
        Pageable pageable = pageable(page, size, sortBy, descending);

        if (StringUtils.hasText(name)) {
            return schemaRepository.findAllByNameContaining(name, pageable).map(this::convert);
        }
        return schemaRepository.findAll(pageable).map(this::convert);
    }

    @Override
    public List<SchemaVO> retrieve(List<Long> ids) {
        if (CollectionUtils.isEmpty(ids)) {
            return schemaRepository.findAll().stream().map(this::convert).toList();
        } else {
            return schemaRepository.findAllById(ids).stream().map(this::convert).toList();
        }
    }

    @Override
    public SchemaVO fetch(Long id) {
        Assert.notNull(id, "id must not be null.");

        return schemaRepository.findById(id).map(this::convert).orElse(null);
    }

    @Override
    public boolean exists(String name, Long id) {
        Assert.hasText(name, "name must not be null.");
        if (id == null) {
            return schemaRepository.existsByName(name);
        }
        return schemaRepository.existsByNameAndIdNot(name, id);
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public SchemaVO create(SchemaDTO dto) {
        Schema schema = convert(dto, Schema.class);
        schema.setEnabled(false);
        schemaRepository.save(schema);
        List<Field> fields = columnMapper.findAllColumnsByName(dto.getName())
                .stream().filter(columnInfo -> !FieldConstant.metadata.contains(columnInfo.getColumnName()))
                .map(columnInfo -> {
                    Field field = new Field();
                    field.setSchemaId(schema.getId());
                    constructField(field, columnInfo);
                    return field;
                }).toList();

        fieldRepository.saveAll(fields);

        return this.convert(schema);
    }

    @Override
    public List<SchemaVO> createAll(Iterable<SchemaDTO> iterable) {
        List<Schema> schemas = StreamSupport.stream(iterable.spliterator(), false)
                .map(dto -> convert(dto, Schema.class)).toList();
        return schemaRepository.saveAll(schemas).stream().map(this::convert).toList();
    }

    @Override
    public SchemaVO modify(Long id, SchemaDTO dto) {
        Assert.notNull(id, "id must not be null.");

        return schemaRepository.findById(id)
                .map(existing -> {
                    Schema schema = convert(dto, existing);
                    schema = schemaRepository.save(schema);
                    return this.convert(schema);
                })
                .orElseThrow();
    }

    @Override
    public void sync(Long id) {
        schemaRepository.findById(id).ifPresent(schema -> {
            Map<String, Field> fieldMap = fieldRepository.findAllBySchemaId(id).stream()
                    .collect(Collectors.toMap(Field::getColumnName, field -> field));

            List<Field> fields = columnMapper.findAllColumnsByName(schema.getName())
                    .stream().filter(columnInfo -> !FieldConstant.metadata.contains(columnInfo.getColumnName()))
                    .map(columnInfo -> {
                        Field field = new Field();
                        if (fieldMap.containsKey(columnInfo.getColumnName())) {
                            field = fieldMap.get(columnInfo.getColumnName());
                        }
                        field.setSchemaId(id);
                        constructField(field, columnInfo);
                        return field;
                    }).toList();

            fieldRepository.saveAll(fields);
        });
    }

    public void constructField(Field field, ColumnInfo columnInfo) {
        field.setColumnName(columnInfo.getColumnName());
        field.setName(snakeToCamel(columnInfo.getColumnName()));
        field.setDataType(columnInfo.getDataType());
        field.setLength(columnInfo.getCharacterMaximumLength());
        FieldTypeEnum fieldTypeEnum = FieldTypeEnum.getByDbType(columnInfo.getDataType());
        if (fieldTypeEnum != null) {
            field.setFieldType(fieldTypeEnum.getJavaType());
            field.setFormType(fieldTypeEnum.getFormType());
            field.setTsType(fieldTypeEnum.getTsType());
        }
        field.setNullable("YES".equals(columnInfo.getNullable()));
        field.setComment(columnInfo.getColumnComment());
    }

    @Override
    public boolean enable(Long id) {
        Assert.notNull(id, "id must not be null.");

        Schema schema = schemaRepository.findById(id).orElseThrow();
        if (schema.isEnabled()) {
            return schema.isEnabled();
        }
        return schemaRepository.updateEnabledById(id);
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void remove(Long id) {
        Assert.notNull(id, "id must not be null.");

        List<Long> ids = fieldRepository.findAllBySchemaId(id).stream().map(Field::getId).toList();
        fieldRepository.deleteAllById(ids);

        schemaRepository.deleteById(id);
    }

    @Override
    public void generate(Long id, OutputStream os) throws IOException, TemplateException {
        Schema schema = schemaRepository.findById(id).orElseThrow();
        // 准备数据模型
        Map<String, Object> dataModel = createDataModel(schema);

        try (ZipOutputStream zos = new ZipOutputStream(os)) {
            for (Long templateId : schema.getTemplates()) {
                // 从数据库加载模板内容
                Template template = templateRepository.findById(templateId).orElseThrow();
                String reference = System.getProperty("user.dir") + "/" + schema.getName() + "/" +
                        formatName(schema, template.getName(), template.getSuffix());
                writeTemplate(template, dataModel, reference);

                File file = new File(reference);
                try (FileInputStream fis = new FileInputStream(file)) {
                    zos.putNextEntry(new ZipEntry(file.getName()));
                    byte[] buffer = new byte[1024];
                    int length;
                    while ((length = fis.read(buffer)) > 0) {
                        zos.write(buffer, 0, length);
                    }
                    zos.closeEntry();
                }
            }
        }
    }

    @Override
    public List<TemplateVO> preview(Long id) throws IOException, TemplateException {
        Schema schema = schemaRepository.findById(id).orElseThrow();
        // 准备数据模型
        Map<String, Object> dataModel = createDataModel(schema);

        Set<Long> templates = schema.getTemplates();
        List<TemplateVO> voList = new ArrayList<>(templates.size());
        for (Long templateId : templates) {
            // 从数据库加载模板内容
            Template template = templateRepository.findById(templateId).orElseThrow();

            String rendered = renderTemplate(template.getName(), template.getContent(), dataModel);
            TemplateVO vo = convert(template, TemplateVO.class);
            vo.setName(formatName(schema, template.getName(), template.getSuffix()));
            vo.setContent(rendered);
            voList.add(vo);
        }
        return voList;
    }

    // 创建数据模型
    private Map<String, Object> createDataModel(Schema schema) {
        Map<String, Object> dataModel = new HashMap<>();
        dataModel.put("name", schema.getName());
        dataModel.put("domain", schema.getDomain());
        dataModel.put("reference", schema.getReference());
        List<Field> fields = fieldRepository.findAllBySchemaId(schema.getId());
        dataModel.put("fields", fields);
        return dataModel;
    }

    // 渲染模板并将结果写入文件
    private void writeTemplate(Template template, Map<String, Object> dataModel, String reference)
            throws IOException, TemplateException {
        // 创建输出目录（若不存在）
        File outputFile = new File(reference);
        boolean mkdir = outputFile.getParentFile().mkdirs();
        if (!mkdir) {
            logger.warn("File path: {} create failure.", reference);
        }

        try (Writer fileWriter = new FileWriter(outputFile)) {
            // 将模板内容加载到 FreeMarker 并渲染
            freemarker.template.Template freemarker = new freemarker.template.Template(template.getName(),
                    new StringReader(template.getContent()), cfg);
            freemarker.process(dataModel, fileWriter);
        }
    }

    private String formatName(Schema schema, String templateName, String suffix) {
        if (templateName.startsWith("%s")) {
            if (".ts".equals(suffix)) {
                if (schema.getName().contains("_")) {
                    return String.format(templateName, schema.getName().replace("_", "-"));
                }
                return String.format(templateName, schema.getName());
            }
            return String.format(templateName, schema.getDomain());
        }
        return templateName;
    }

    // 渲染模板并将结果写入文件
    private String renderTemplate(String name, String content, Map<String, Object> dataModel) throws IOException, TemplateException {
        // 将模板内容加载到 FreeMarker 并渲染
        try (StringWriter stringWriter = new StringWriter()) {
            freemarker.template.Template freemarker = new freemarker.template.Template(name,
                    new StringReader(content), cfg);
            freemarker.process(dataModel, stringWriter);
            return stringWriter.toString();
        }
    }

    private String snakeToCamel(String columnName) {
        if (StringUtils.hasText(columnName)) {
            if (columnName.contains("_")) {
                String[] columns = columnName.split("_");
                StringBuilder fieldName = new StringBuilder(columns[0]);
                for (int i = 1; i < columns.length; i++) {
                    fieldName.append(columns[i].substring(0, 1).toUpperCase()).append(columns[i].substring(1));
                }
                return fieldName.toString();
            }
        }
        return columnName;
    }

    /**
     * 类型转换
     *
     * @param schema 信息
     * @return 输出对象
     */
    private SchemaVO convert(Schema schema) {
        SchemaVO vo = convert(schema, SchemaVO.class);
        vo.setLastModifiedDate(schema.getLastModifiedDate().orElse(null));
        return vo;
    }
}
